64 — 有趣又简单的 R3F 作品集

Three.js Journey · Bruno Simon · 难度:简单

64 — 有趣又简单的 R3F 作品集 课程海报

介绍

如果你看到这里,并且正试图用 Three.js 提升自己的 WebGL 技能,那你大概知道拥有一个优秀的作品集有多么重要。

在网页开发中,更广泛地说在数字行业中,最重要的是你的经验。公司和招聘人员在给你机会之前,肯定会首先根据这一标准来评判你。

你需要一种方式来展示你的项目,因此拥有一个作品集很重要。

而这个作品集本身其实就是你的项目之一(如果不是最重要的那个的话)。它应该成为你的见证,展示你能做什么,以及公司为什么要聘用你。

但创建一个酷炫的作品集是一个漫长而艰难的过程。你是你自己的客户,而且你很可能是你能遇到的最苛刻的客户。

不过,假设你已经有了一个更简单的 HTML/CSS 作品集,用来展示你的项目和一些信息(这已经很不错了)。

在这节课中,我们将在这个 HTML/CSS 作品集的基础上加以利用,让它看起来更令人兴奋、更有创意。我们将创建一个漂亮的 3D 场景,里面有一台笔记本电脑模型,并把那个 HTML/CSS 作品集放进笔记本电脑的屏幕里。

lesson demo video preview

这个概念的灵感来自 Paul Henschel(0xca0a)的实验,你可以在这里找到它 https://twitter.com/0xca0a/status/1398633764931178498

请注意,在整个课程中,我们会设置一些恰到好处的值,让体验令人愉悦。为了得到这些值,我不得不反复调整,并且我使用 Leva 来完成。

如果你正在创建自己的体验,不要因为没能第一次就找到完美的值而感到沮丧。这是一个反复试验的过程。别忘了添加一个像 Leva 这样的调试 UI 来帮助你。

初始设置

在起始项目里,我们在中心有一个简单的立方体。

@react-three/drei 依赖已经安装在项目中了,我们使用 OrbitControls 辅助工具来绕相机移动。我们稍后会替换它,但现在先保留它。

我们没有使用 Perf,因为我们不应该有任何性能方面的顾虑,但如果你愿意,可以随意添加它。

我们没有使用 Leva,因为我们会设置非常具体的值,但如果你想调整体验,应该添加它。

背景

首先,让我们把背景改成更深的颜色,以营造更温暖的氛围,并突出内容的重要性。

在 Experience.jsx 中,添加一个带有 attach 属性并设为 "background" 的 <color>:

export default function Experience()
{
    return <>

        <color args={ [ '#241a1a' ] } attach="background" />

        {/* ... */}

    </>
}

笔记本电脑

我们将把作品集显示在一台笔记本电脑上。

我们可以自己创建这个模型,但不如使用一个免费的模型。

找一个好模型

网上有很多免费的模型,但 PMNDRS 团队创建了一个地方,我们可以在那里找到一系列可以直接在 Three.js 中、更具体地说是在 R3F 中使用的模型:https://market.pmnd.rs

如果你在找 "Macbook" 模型,你应该能找到这个 https://market.pmnd.rs/model/macbook。

大多数模型都在 CC0 许可证(“保留所有权利”的反面)下提供,这意味着你可以对它们做任何你想做的事。

顺便说一下,如果你想帮忙,可以向 PMNDRS Market 提交你自己的模型。

实现模型

我们有多种选择可以把模型引入我们的体验:

让我们保持简单,使用 CDN。

点击“copy direct link”。

在 Experience.jsx 中,从 @react-three/drei 导入 useGLTF:

import { useGLTF, OrbitControls } from '@react-three/drei'

然后用 useGLTF 加载模型(使用你从 PMNDRS Market 复制的链接,而不是下面这个,即使它们是同一个模型):

export default function Experience()
{
    const computer = useGLTF('https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/macbook/model.gltf')
    
    // ...
}

如果由于某些原因 https://market.pmnd.rs 无法访问,你可以使用以下链接:

'https://threejs-journey.com/resources/models/macbook_model.gltf'

最后,用 <primitive> 及其 object 属性添加它:

export default function Experience()
{
    // ...

    return <>

        {/* ... */}

        <primitive object={ computer.scene } />

    </>
}

你应该会看到一台全黑的笔记本电脑,因为我们需要灯光。

灯光与环境

我们将使用环境光照,并且通过使用来自 drei 的 Environment 辅助工具并搭配一个预设,超级偷懒一番。

从 @react-three/drei 导入 Environment:

import { Environment, useGLTF, OrbitControls } from '@react-three/drei'

把它添加到场景中的任意位置,并将 "city" 作为 preset 属性:

export default function Experience()
{
    // ...

    return <>

        <Environment preset="city" />

        {/* ... */}

    </>
}

现在我们可以借助合适的光源来欣赏这个模型了。

既然我们终于能看见东西了,那就把立方体移除:

漂浮动画

我们将让笔记本电脑缓慢移动,但首先,让我们把它放到场景的正中央。

我们本可以使用 <Center> 辅助工具,但那样会导致整台电脑被居中,其屏幕会有点偏。

相反,我们将把 position-y 设为 - 1.2:

<primitive
    object={ computer.scene }
    position-y={ - 1.2 }
/>

现在我们可以让笔记本电脑漂浮起来了,我们将使用 drei 辅助工具中的 Float。

首先,从 @react-three/drei 导入 Float:

import { Float, Environment, useGLTF, OrbitControls } from '@react-three/drei'

然后,把 <primitive> 包在 <Float> 里:

<Float>
    <primitive
        object={ computer.scene }
        position-y={ - 1.2 }
    />
</Float>
lesson demo video preview

默认的漂浮动画感觉不错,但请记住我们要在屏幕上显示一个 HTML/CSS 网站。如果屏幕移动得太多,你的网站访客就无法与网站交互了。

用 rotationIntensity 属性降低旋转强度:

<Float rotationIntensity={ 0.4 } >
    <primitive
        object={ computer.scene }
        position-y={ - 1.2 }
    />
</Float>
lesson demo video preview

相机与控制

目前,我们使用的是来自 drei 的 OrbitControls,我们可以绕模型全方位旋转。

这没什么大不了的,但最好是限制旋转,同时仍然允许用户稍微旋转一下。

与其调整 OrbitControls 的属性,我们将使用一个名为 PresentationControls 的 drei 辅助工具。

PresentationControls 让你操作的是模型而不是相机。我们可以旋转那个模型,而当我们松开它时,它会回到初始位置。

在内部,它使用了 Spring;这是另一个 PMNDRS 库,它让动画变得真实而令人愉悦。

实现

从 @react-three/drei 导入 PresentationControls:

import { PresentationControls, Float, Environment, useGLTF, OrbitControls } from '@react-three/drei'

现在移除 <OrbitControls> 及其导入,因为我们不希望两个不同的控制器相互干扰。

最后,把 <PresentationControls> 添加到 <Float> 周围:

export default function Experience()
{
    // ...
    
    return <>

        <color args={ [ '#241a1a' ] } attach="background" />

        <Environment preset="city" />
        
        <PresentationControls>
            <Float rotationIntensity={ 0.4 } >
                <primitive
                    object={ computer.scene }
                    position-y={ - 1.2 }
                />
            </Float>
        </PresentationControls>

    </>
}
lesson demo video preview

默认行为感觉很像 OrbitControls,但我们可以对它加以改进。

全局

默认情况下,我们只能通过在模型上直接拖放来旋转物体。

我们希望能够从任何地方拖放,甚至是背景。

要做到这一点,添加 global 属性:

<PresentationControls
    global
>
    {/* ... */}
</PresentationControls>
lesson demo video preview

修复移动端手势错误

如果你现在拖放物体,你可能会注意到控制台中出现一个警告:

即使你没有看到这个错误,下面的问题也是存在的。

PresentationControls 使用了 use-gesture,这是一个使用自然手势(比如拖放)与元素交互的库,该库建议把 CSS 属性 touch-action 设为 none,以修复移动端在滑动时的怪异行为。

确实,如果你在移动端测试当前的体验,你会注意到拖放有点笨拙,并且会与浏览器默认的手势冲突,比如从上到下拖放时重新加载页面。

你可以在这里找到关于此问题的更多信息:https://use-gesture.netlify.app/docs/extras/#touch-action

按照那个网站上所说的,我们需要在 CSS 中添加 touch-action 属性,但需要在处理指针事件的元素上添加,也就是 R3F 创建的、正好位于 <canvas> 上方的那个 <div>。

默认情况下,这个 <div> 没有 class,但我们可以通过在 <Canvas> 上使用 className 来设置一个:

<Canvas
    className="r3f"
    camera={ {
        fov: 45,
        near: 0.1,
        far: 2000,
        position: [ -3, 1.5, 4 ]
    } }
>
    <Experience />
</Canvas>

我们选择了 "r3f",但你可以改成任何你想要的。

接下来,在 /src/style.css 中,我们可以选中它并把它的 touch-action 改为 none:

.r3f
{
    touch-action: none;
}

旋转

我们可以用 rotation 属性更改默认旋转:

<PresentationControls
    global
    rotation={ [ 0.13, 0.1, 0 ] }
>
    {/* ... */}
</PresentationControls>

限制

我们可以同时限制水平和垂直方向的旋转。

垂直方向的那个叫做 polar,我们必须传入一个包含两个值的数组,即最小值和最大值:

<PresentationControls
    global
    rotation={ [ 0.13, 0.1, 0 ] }
    polar={ [ - 0.4, 0.2 ] }
>
    {/* ... */}
</PresentationControls>
lesson demo video preview

水平方向的那个叫做 azimuth,用法相同:

<PresentationControls
    global
    rotation={ [ 0.13, 0.1, 0 ] }
    polar={ [ - 0.4, 0.2 ] }
    azimuth={ [ - 1, 0.75 ] }
>
    {/* ... */}
</PresentationControls>
lesson demo video preview

Spring 配置

最后,我们想调整动画的物理效果。

正如我们之前所说,PresentationControls 使用了 Spring 来做动画,我们可以调整那个 spring 的属性。

我们不打算详细讲解 spring 的所有属性,但你可以在这里查看文档 https://react-spring.dev/common/configs#configs

有两种不同的 spring 配置。

拖放时使用的那一个可以用 config 属性设置:

<PresentationControls
    global
    rotation={ [ 0.13, 0.1, 0 ] }
    polar={ [ - 0.4, 0.2 ] }
    azimuth={ [ - 1, 0.75 ] }
    config={ { mass: 2, tension: 400 } }
>
    {/* ... */}
</PresentationControls>
lesson demo video preview

松开时使用的那一个可以用 snap 属性设置:

<PresentationControls
    global
    rotation={ [ 0.13, 0.1, 0 ] }
    polar={ [ - 0.4, 0.2 ] }
    azimuth={ [ - 1, 0.75 ] }
    config={ { mass: 2, tension: 400 } }
    snap={ { mass: 4, tension: 400 } }
>
    {/* ... */}
</PresentationControls>
lesson demo video preview

阴影

为了增加场景的真实感,我们将添加一个阴影。

正如我们在之前的课程中看到的,有很多选项可供我们选择,但让我们保持简单,使用来自 drei 的 ContactShadows。

首先,从 @react-three/drei 导入 ContactShadows:

import { ContactShadows, PresentationControls, Float, Environment, useGLTF } from '@react-three/drei'

然后,把它添加到 <PresentationControls> 外面(我们不希望它旋转),并用 position-y 属性把它向下移动一点:

export default function Experience()
{
    // ...
    
    return <>

        {/* ... */}
        
        <PresentationControls
            global
            rotation={ [ 0.13, 0.1, 0 ] }
            polar={ [ - 0.4, 0.2 ] }
            azimuth={ [ - 1, 0.75 ] }
            config={ { mass: 2, tension: 400 } }
            snap={ { mass: 4, tension: 400 } }
        >
            {/* ... */}
        </PresentationControls>

        <ContactShadows position-y={ - 1.4 } />

    </>
}

默认情况下它看起来还不错,但让我们稍微调整一下。

更改它的 opacity、scale 和 blur 属性:

<ContactShadows
    position-y={ - 1.4 }
    opacity={ 0.4 }
    scale={ 5 }
    blur={ 2.4 }
/>

iframe

是时候添加真正的 HTML/CSS 网站了。

我们希望它显示在我们的模型上,就好像它是真正的屏幕一样。

HTML/CSS 网站

首先,我们需要一个真正的 HTML/CSS 网站。

你可以使用你自己的,但作为示例,我们将使用我的作品集的一个 HTML/CSS 版本,你可以在这里找到它 https://bruno-simon.com/html/

如果你使用自己的网站,请确保服务器允许你把这些 URL 用作 iframe(如果有问题,你马上就会看到)。

实现

我们将使用一个 <iframe> 把那个网站放入我们的体验中。

如果你不知道 iframe 是什么,可以把它想象成一扇通向另一个网站的窗口。它使得网站可以在其他网站上显示(网站套娃)。

但别忘了,我们希望那个 iframe 跟随模型,就好像它是我们 WebGL 场景的一部分一样。

要做到这一点,我们将使用来自 drei 的 Html 辅助工具:

首先,从 @react-three/drei 导入 Html:

import { Html, ContactShadows, PresentationControls, Float, Environment, useGLTF } from '@react-three/drei'

现在,把它添加到 <primitive> 里面,这样 HTML 就会贴着笔记本电脑模型:

<primitive
    object={ computer.scene }
    position-y={ - 1.2 }
    rotation-x={ 0.13 }
>
    <Html>This is going to be an iframe</Html>
</primitive>

现在我们可以把文本替换成一个 <iframe>,其 src 属性指向我们的 HTML/CSS 网站:

<Html>
    <iframe src="https://bruno-simon.com/html/" />
</Html>

它生效了,但我们还有一些改进要做。

首先,我们希望 iframe 能够自己进行变换,就好像它是场景的一部分,而不仅仅是跟随一个 3D 点。

变换

要做到这一点,添加 transform 属性:

<Html
    transform
>
    {/* ... */}
</Html>
lesson demo video preview

尺寸

目前,我们看到的是一个非常小的 <iframe>,但被超级拉伸了(如果这说得通的话)。

首先,我们要确定一个尺寸,让 iframe 里的内容看起来像样。我们现在不关心它在场景中占多大,我们只希望内容能合适地放进 iframe 里。要做到这一点,我们需要对它应用一些 CSS。

让我们给它添加一个类,以便在 CSS 中控制它。

添加一个 wrapperClass 属性,内容为 htmlScreen:

<Html
    transform
    wrapperClass="htmlScreen"
>
    {/* ... */}
</Html>

现在,iframe 的父元素上就有了一个 htmlScreen 类。

然后,在 /src/style.css 中,添加 width 和 height 属性:

.htmlScreen iframe
{
    width: 1024px;
    height: 670px;
}
lesson demo video preview

真是一团糟!

别担心,我们现在需要做的就是把最终元素变小。

我们可以用 distanceFactor 属性做到这一点:

<Html
    transform
    wrapperClass="htmlScreen"
    distanceFactor={ 1.17 }
>
    {/* ... */}
</Html>
lesson demo video preview

小修复

能够在 CSS 中控制 iframe 的外观,是一个修复小问题的好机会。

首先,我们可以移除 iframe 的默认边框:

.htmlScreen iframe
{
    width: 1024px;
    height: 670px;
    border: none;
}

虽然如果你是 Apple 粉丝的话会觉得不准确,但我们要用 border-radius 在角落里添加一个小斜面:

.htmlScreen iframe
{
    width: 1024px;
    height: 670px;
    border: none;
    border-radius: 20px;
}

最后,我们要给它添加一个黑色背景,以防 iframe 中的 CSS 加载得不够快:

.htmlScreen iframe
{
    width: 1024px;
    height: 670px;
    border: none;
    border-radius: 20px;
    background: #000000;
}

位置与旋转

现在我们可以用 position 属性把 iframe 移到模型的屏幕上了:

<Html
    transform
    wrapperClass="htmlScreen"
    distanceFactor={ 1.17 }
    position={ [ 0, 1.56, - 1.4 ] }
>
    {/* ... */}
</Html>

然后,用 rotation-x 让它绕 x 轴旋转:

<Html
    transform
    wrapperClass="htmlScreen"
    distanceFactor={ 1.17 }
    position={ [ 0, 1.56, - 1.4 ] }
    rotation-x={ - 0.256 }
>
    {/* ... */}
</Html>

屏幕灯光

事情开始看起来真的很不错了。我们将在屏幕位置添加一个橙色的 RectAreaLight,以增加更多的真实感,这样我们就能看到光反射在笔记本电脑键盘上。

在 <Float> 里面添加一个 <rectAreaLight>,并带有以下属性:

<Float rotationIntensity={ 0.4 } >
                
    <rectAreaLight
        width={ 2.5 }
        height={ 1.65 }
        intensity={ 65 }
        color={ '#ff6900' }
        rotation={ [ - 0.1, Math.PI, 0 ] }
        position={ [ 0, 0.55, - 1.15 ] }
    />

    {/* ... */}

</Float>

你可以根据正在显示的页面随意更改颜色。

文本

我们快要完成了,但我们的项目对它的用途还缺乏清晰的理解。我们是在卖电脑吗?

为了让人们明显地看出这个网站代表的是你的作品集,我们将把我们的名字添加到右上角部分,并且我们将使用来自 drei 的 Text 辅助工具。

实现

从 @react-three/drei 导入 Text:

import { Text, Html, ContactShadows, PresentationControls, Float, Environment, useGLTF } from '@react-three/drei'

把它添加到 <Float> 里面,并放入你的名字或任何你想要的内容:

<Float rotationIntensity={ 0.4 } >

    {/* ... */}

    <Text>BRUNO SIMON</Text>

</Float>

你应该能看到文本,但要小心,iframe 位于 <canvas> 之上。

这没什么大不了的,因为我们要把它移到右上角。

字体

来自 Google Fonts 的 Bangers 字体可以在 /public/ 文件夹中找到。

它看起来有点傻气、像卡通一样,营造出一种有趣的对比。

你可以随意使用任何你喜欢的字体,并记住你可以用 Google Webfonts Helper 从 Google Fonts 下载字体。

用 font 属性设置字体,并用 fontSize 属性更改其大小:

<Text
    font="./bangers-v20-latin-regular.woff"
    fontSize={ 1 }
>
    BRUNO SIMON
</Text>

位置与旋转

用以下 position 和 rotation 属性把它移到一边:

<Text
    font="./bangers-v20-latin-regular.woff"
    fontSize={ 1 }
    position={ [ 2, 0.75, 0.75 ] }
    rotation-y={ - 1.25 }
>
    BRUNO SIMON
</Text>

换行

我们希望文本能分成两行,我们可以通过用 maxWidth 属性设置一个受限的宽度来做到这一点:

<Text
    font="./bangers-v20-latin-regular.woff"
    fontSize={ 1 }
    position={ [ 2, 0.75, 0.75 ] }
    rotation-y={ - 1.25 }
    maxWidth={ 2 }
>
    BRUNO SIMON
</Text>
lesson demo video preview

如果你的名字长度与姓氏相差很大,你也可以调整 textAlign 属性,让文本与场景很好地契合。

更进一步

就这样!在一个小时内,我们为我们的 HTML/CSS 作品集创建了一个好看的替代方案。

请记住,这只是一个快速而简单的方案,我们还可以走得更远。

以下是一些改进的想法: